Computing in School: An Introduction to Classes

Computing in Schools: The Basics of OOP

In this post I will build up a collection of classes and objects using simple concepts of shape that all pupils should be familiar with. For example we will start with a shape class and build up triangles and quadriallterals classes that will inhert from the shape class, then a square class etc.

1. Create a class called Shape

We create a basic root class called Shape. We give it a constructor method. In python this is the __init__() method. The constructor to define the number of sides the shape has. The constructor method should take as input an integer to specify the number of sides it has. This should then be declared as an attribute within the method. We will also declare a name attribute within the constructor

class Shape:
    def __init__(self, given_num_sides):
        self.num_sides = given_num_sides
        self.name = "shape"

Note that I am going to use a naming convention whereby all variable names passed into a function will be prefixed with the word "given". For example given_num_sides is passed to the constructor so it can create the attribute num_sides. In practice I would probably just use the same name for both, but the exam mark schemes seem to prefer different names.

2. Create a Triangle class

The Triangle class should inherit from the Shape class. It needs its own constructor method. The contructor for Triangle should take as input a base and a height and declare these as attributes. The constructor should call the constructor of the class it inherits from. To do this in python we use the super() function. This super function needs to take as input the integer 3 which the Shape class will use to define a number of sides attribute.

class Triangle(Shape):
    def __init__(self, given_base, given_height):
        super().__init__(3)
        self.base = given_base
        self.height = given_height
        self.name = "triangle"

3. Add an area method to the Triangle class

We add a method to the Triangle class to calculate a the value of the area. Note that when using the values for the base and height within the class we need to use the self keyword that references the values of the object rather than the class. For example

class Triangle(Shape):
    def __init__(self, given_base, given_height):
        super().__init__(3)
        self.base = given_base
        self.height = given_height
        self.name = "triangle"

    def area(self):
        return 0.5 * self.base * self.height

Note that we could have given the triangle and area attribute instead of a method like so

class Triangle(Shape):
    def __init__(self, given_base, given_height):
        super().__init__(3)
        self.base = given_base
        self.height = given_height
        self.name = "triangle"
        self.area = 0.5 * given_base * given_height

4. Test everything works so far.

We will now instantiate (which just means create) a shape object from the Shape class and print its attributes. We will also instantiate a triangle object from the Triangle class (assuming we are using the first version of Triangle), print its attributes and call its area method. Note that even though we have not passed the number 3 as input when creating the triangle object its num_sides attribute will still be set to three inside the constructor.

shape = Shape(5)
print(shape.name)
print(shape.num_sides)

triangle = Triangle(4, 5)
print(triangle.name)
print(triangle.num_sides)
print(triangle.base)
print(triangle.height)
print(triangle.area())

5. Create a Quadrillateral class

We create a Quadrillateral class that inherits from the Shape class. Everything is very similar to the Triangle class, i.e., it should have a constructor method that calls the super classes constructor with the number of sides as 4. The point of doing this is that if in future we decide that all quadrillaterals should have something in common they we only need to modify the quadrillateral class and not all the classes that inherit from it.

class Quadrillateral(Shape):
    def __init__(self):
        super().__init__(4)
        self.name = "quadrillateral"

6. Create a Square class

We will now create a class for a Square that should inherit from the Quadrillateral class. As usual it will need a constructor method and some attributes set. We add a list as an attribute to hold values for the angles of the square. We also add an area method.

class Square(Quadrillateral):
    def __init__(self, given_length):
        super().__init__()
        self.angles = [90, 90, 90, 90]
        self.length = given_length
        self.name = "Square"

    def area(self):
        return self.length **2

At this point you might be thinking that actually all quadrillaterals have 4 angles so the Quadrillateral class should have an attribute to hold the angles. Or for that matter the Shape class should have an attribute for the angles. We will go ahead now and modify the classes to make this work. Note that angles will need to be passed through the constructor methods. This is an example of what programmers call refactoring, i.e., changing the structure of their programme without changing its functionality. This could be to make it more readable or maintainable for example. The new classes now look like this

class Shape:
    def __init__(self, given_num_sides, given_angles):
        self.num_sides = given_num_sides
        self.name = "shape"
        self.angles = given_angles

class Triangle(Shape):
    def __init__(self, given_base, given_height, angles):
        super().__init__(3, angles)
        self.base = given_base
        self.height = given_height
        self.name = "triangle"

    def area(self):
        return 0.5 * self.base * self.height

class Quadrillateral(Shape):
    def __init__(self, given_angles):
        super().__init__(4, given_angles)
        self.name = "quadriallateral"

class Square(Quadrillateral):
    def __init__(self, given_length):
        super().__init__([90, 90, 90, 90])
        self.length = given_length
        self.name = "Square"

7 Test some more

Now that the classes have been refactored we will test them all to make sure they work

shape = Shape(5, [30, 40, 50, 60, 70])
print(shape.name)
print(shape.num_sides)
print(shape.angles)

triangle = Triangle(4, 5, [30, 60, 90])
print(triangle.name)
print(triangle.num_sides)
print(triangle.base)
print(triangle.height)
print(triangle.area())

square = Square(4)
print(square.name)
print(square.num_sides)
print(square.length)
print(square.area())

8. Validation

You might have noticed that it is possible to define shapes that have angles that are not consistent. For example, there is nothing to stop us defining a triangle with angles of 10, 20 and 30. We will add a validation method to the shape class that will raise an error if the sum of the angles are not consistent with the number of sides of the shape. We also check to make sure the shape has at least 3 sides. We have included this in the Shape class so that all inherited classes will gain this method. The validator method is called within the constructor for the shape class. You might be able to think of other validations that need to be made to make sure only consistent shapes are created.

class Shape:
    def __init__(self, given_num_sides, given_angles):
        self.num_sides = given_num_sides
        self.name = "shape"
        self.angles = given_angles
        self.angle_sum = (self.num_sides - 2) * 180
        self.validator()

    def validator(self):
        if sum(self.angles) != self.angle_sum:
        raise ValueError("Angles must sum to "+str(self.angle_sum))
        if self.num_sides < 3:  
        raise ValueError("The shape must have at least 3 sides")
        if self.num_sides != len(self.angles):
        raise ValueError("The number of sides must be the same as the number of angles")

9. Encapsulation

We will add a private attribute to the Shape class. In python we doo this by adding a double underscore infront of the name. This attribute will only be able to be modified or accessed by methods within the class itself. The follwoing code should result in an error as we have tried to access a private attribute

class Shape:
    def __init__(self, n, angles):
        self.__secret = "You can't see me"

    shape = Shape(3, [30, 60, 90])
    shape.__secret

This is called encapsulation. The secret attitribute is encapsualted with the class. We can however allow access to this attribute from outside the class by writting what are called getter and setter methods. Getter's get the value of attribute and setter's change the value of the attribute. This makes it more unlikelikly that this attribute will be accessed or changed by accident (although not impossible). The Shape class now looks like

class Shape:
    def __init__(self, given_num_sides, given_angles):
        self.num_sides = given_num_sides
        self.name = "shape"
        self.angles = given_angles
        self.angle_sum = (self.num_sides - 2) * 180
        self.validator()
        self.__secret = "You can't see me"

    def get_secret(self):
        return self.__secret

    def set_secret(self, given_secret):
        self.__secret = given_secret

    def validator(self):
        if sum(self.angles) != self.angle_sum:
        raise ValueError("Angles must sum to "+str(self.angle_sum))
        if self.num_sides < 3:  
        raise ValueError("The shape must have at least 3 sides")
        if self.num_sides != len(self.angles):
        raise ValueError("The number of sides must be the same as the number of angles")

10. Create a Tracker class

We will now create a class called Tracker thats purpose is to keep track of all the objects we create. This class will not inherit from Shape as it has nothing in common with it. Its constructor method will create an attribute called created_objects that will be an empy list and used for storing the shape objects we create. We will make it private by adding the double underscore as we don't want it to be accidentally changed. We add a setter method called add that will add an object to the list and a getter method called summarise that will print out the details of the objects. Note that neither the getter or settr methods access the created_objects attribute in its raw form.

class Tracker:
    def __init__(self):
        self.__created_objects = []

    def add(self, obj):
        if obj not in self.__created_objects:
        self.__created_objects.append(obj)

    def summarise(self):
        for obj in self.__created_objects:
        print(obj.name, obj.num_sides)

We now test it by instantiating an instance of the Triangle and Square classes then instantiating an instance of the Tracker class then calling the add method to add the triangle and square objects to the tracker object. We then call the summarise method to list the details of the stored objects.

triangle = Triangle(3, 4, [30, 60, 90])
square = Square(4)
tracker = Tracker()
tracker.add(triangle)
tracker.add(square)
tracker.summarise()

More functionality could be added to the summarise method such as prettier printing with column titles for example and/or more details about the shapes.

11. Polymorphism

Let's say we would like to write a method that prints out all of the attributes of an object. For the shape class this might look something like

def print_all(self):
    print("This shape has " + str(self.num_sides))
    print("The angles of the shape are " ,self.angles)
    print("The sum of the angles is " + str(self.angle_sum))

All classess that inherit from Shape will gain this method. However, subclasses of Shape may need to print out additional information. The Triangle class for example may need to print out its base and height. We will need to rewrite the print_all method for the Triangle class. This is an example of method overiding. The print_all method in the Triangle class "overirdes" the inherited version of the print_all method it got from the Shape class. This is an example of how the print_all method could be implemented in the Triangle class

def print_all(self):
    super.print_all()
    print("The triangle has a height of " + str(self.height))
    print(" The triangle has a base of " + str(self.base))

Note we have made a call to the print_all function in the super class to avoid repeating code.

12. Further suggestions

This activity could be taken much further. We could implement methods to draw the shapes. methods to calculate other aspects of the shapes etc. Please comment below if you have any good suggestions. I am constantly modifying this every time I use it with pupils. If your have any suggestions then please post in the comments below.

Comments